using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using Assets.Code;

[RequireComponent(typeof(Collider2D))]
public class ViewOf2D : MonoBehaviour
{
    private Collider2D selfCollider;
    private MeshFilter viewMeshFilter;
    private Camera viewMeshCamera;
    private Transform viewMeshCameraTransform;
    private ViewMeshCreater viewMeshCreater;

    private RenderTexture viewMeshCameraRenderTexture;
    private Texture2D viewMeshCameraTexture;

    public bool debug;

    public LayerMask blockLayerMask;
    public float radius = 20;
    public float detectionOffset = 0.02f;

    public Vector2Int viewMeshCameraResolution = new Vector2Int(4096, 4096);

    [System.NonSerialized]
    public Sprite resultSprite;

    public Vector2 detectionDepthSection;

    public string viewMeshLayer;

    public bool fanShape;
    public float fanAngle = 90;

    private void Awake()
    {
        viewMeshCreater = new ViewMeshCreater();
        selfCollider = GetComponent<Collider2D>();

        viewMeshCameraRenderTexture = new RenderTexture(viewMeshCameraResolution.x, viewMeshCameraResolution.y, 0);
        viewMeshCameraRenderTexture.enableRandomWrite = true;

        viewMeshCameraTexture = new Texture2D(viewMeshCameraRenderTexture.width, viewMeshCameraRenderTexture.height, TextureFormat.ARGB32, false);
        resultSprite = Sprite.Create(viewMeshCameraTexture, new Rect(0, 0, viewMeshCameraTexture.width, viewMeshCameraTexture.height), Vector2.one * 0.5f);
    }

    private void Start()
    {
        GameObject viewMeshPrefab = (GameObject)Resources.Load("Prefabs/ViewMesh");
        GameObject viewMeshCameraPrefab = (GameObject)Resources.Load("Prefabs/ViewMeshCamera");

        GameObject viewMesh = Instantiate(viewMeshPrefab);
        GameObject viewMeshCamera = Instantiate(viewMeshCameraPrefab);

        viewMesh.layer = LayerMask.NameToLayer(viewMeshLayer);
        viewMeshFilter = viewMesh.GetComponent<MeshFilter>();

        this.viewMeshCamera = viewMeshCamera.GetComponent<Camera>();
        this.viewMeshCamera.cullingMask = 1 << viewMesh.layer;
        viewMeshCameraTransform = viewMeshCamera.GetComponent<Transform>();
        this.viewMeshCamera.targetTexture = viewMeshCameraRenderTexture;
        this.viewMeshCamera.orthographicSize = radius;
    }

    private void LateUpdate()
    {
        UpdateView();
    }

    private void UpdateView()
    {
        List<Vector2> points = GetVisiblePointsOfColliders();
        List<HitInfo> hitInfos = points.ConvertAll(point => new HitInfo(point, false));
        //print(hitInfos.Count);

        if (fanShape)
        {
            Vector2 rightDirection = Quaternion.AngleAxis(fanAngle / 2, Vector3.forward) * transform.up;
            Vector2 leftDirection = Quaternion.AngleAxis(-fanAngle / 2, Vector3.forward) * transform.up;
            RaycastHit2D rightRaycastHit = Physics2D.Raycast(transform.position, rightDirection, radius, blockLayerMask);
            RaycastHit2D leftRaycastHit = Physics2D.Raycast(transform.position, leftDirection, radius, blockLayerMask);
            if (rightRaycastHit)
                hitInfos.Add(new HitInfo(rightRaycastHit.point, true));
            else
                hitInfos.Add(new HitInfo((Vector2)transform.position + rightDirection * radius, true));
            if (leftRaycastHit)
                hitInfos.Add(new HitInfo(leftRaycastHit.point, true));
            else
                hitInfos.Add(new HitInfo((Vector2)transform.position + leftDirection * radius, true));
        }
        else
        {
            Vector2[] rectPoint = new Vector2[] {
            (Vector2)transform.position + Vector2.up * radius,
            (Vector2)transform.position + Vector2.down * radius,
            (Vector2)transform.position + Vector2.left * radius,
            (Vector2)transform.position + Vector2.right * radius};

            foreach (Vector2 point in rectPoint)
            {
                if (IsVisiblePoint(point, false))
                    hitInfos.Add(new HitInfo(point, true));
            }
        }

        hitInfos = hitInfos.OrderBy(hitInfo => GetAngle360((hitInfo.basicPoint - (Vector2)transform.position).normalized, Vector2.right)).ToList();

        hitInfos.ForEach(hitInfo =>
        {
            CompleteHitInfo(hitInfo);
        });
        viewMeshCreater.Clear();
        viewMeshCreater.SetCenter(transform.position);
        for (int i = 0; i < hitInfos.Count; i++)
        {
            int nextIndex = (i + 1) % hitInfos.Count;
            HitInfo hitInfo = hitInfos[i];
            HitInfo nextInfo = hitInfos[nextIndex];
            Vector2 point1 = GetHitRightFixedPoint(hitInfo);
            Vector2 point2 = GetHitLeftFixedPoint(nextInfo);
            viewMeshCreater.AddNewTriangle(point1 - (Vector2)transform.position, point2 - (Vector2)transform.position);
        }
        viewMeshFilter.mesh = viewMeshCreater.GetMesh();
        viewMeshCameraTransform.position = new Vector3(transform.position.x, transform.position.y, viewMeshCameraTransform.position.z);
        viewMeshCamera.Render();
        Graphics.ConvertTexture(viewMeshCameraRenderTexture, viewMeshCameraTexture);
    }

    private Vector2 GetHitRightPoint(HitInfo hitInfo)
    {
        if (hitInfo.isAssistPoint)
            return hitInfo.basicPoint;
        if (hitInfo.rightHit)
            return hitInfo.rightHitPoint;
        else
            return (Vector2)transform.position + hitInfo.toRightPointDirection * radius;
    }
    private Vector2 GetHitLeftPoint(HitInfo hitInfo)
    {
        if (hitInfo.isAssistPoint)
            return hitInfo.basicPoint;
        if (hitInfo.leftHit)
            return hitInfo.leftHitPoint;
        else
            return (Vector2)transform.position + hitInfo.toLeftPointDirection * radius;
    }

    private Vector2 GetHitRightFixedPoint(HitInfo hitInfo)
    {
        Vector2 point = GetHitRightPoint(hitInfo);
        if (hitInfo.isAssistPoint)
            return point;
        float toPointDistance = Vector2.Distance(point, transform.position);
        Vector2 fixedPoint = (Vector2)transform.position + hitInfo.toPointDirection * toPointDistance;
        if (!hitInfo.rightHit)
            return fixedPoint;

        Vector2 point1Perpendicular = Vector2.Perpendicular(hitInfo.rightHitNormal);
        float pointA = Vector2.Angle(point1Perpendicular, -hitInfo.toPointDirection);
        if (pointA > 90)
            pointA = Vector2.Angle(-point1Perpendicular, -hitInfo.toPointDirection);
        if (pointA == 0)
            return fixedPoint;

        float pointa = PointToRayDistance(point, point1Perpendicular, fixedPoint);
        float point1c = pointa / Mathf.Sin(pointA * Mathf.Deg2Rad);

        if (Vector2.Dot((fixedPoint - point).normalized, hitInfo.rightHitNormal) < 0)
            return fixedPoint - hitInfo.toPointDirection * point1c;
        else
            return fixedPoint + hitInfo.toPointDirection * point1c;
    }

    private Vector2 GetHitLeftFixedPoint(HitInfo hitInfo)
    {
        Vector2 point = GetHitLeftPoint(hitInfo);
        if (hitInfo.isAssistPoint)
            return point;
        float toPointDistance = Vector2.Distance(point, transform.position);
        Vector2 fixedPoint = (Vector2)transform.position + hitInfo.toPointDirection * toPointDistance;
        if (!hitInfo.leftHit)
            return fixedPoint;
        Vector2 point1Perpendicular = Vector2.Perpendicular(hitInfo.leftHitNormal);

        float pointA = Vector2.Angle(point1Perpendicular, -hitInfo.toPointDirection);
        if (pointA > 90)
            pointA = Vector2.Angle(-point1Perpendicular, -hitInfo.toPointDirection);
        if (pointA == 0) {
            return fixedPoint;
        }

        float pointa = PointToRayDistance(point, point1Perpendicular, fixedPoint);
        
        float point1c = pointa / Mathf.Sin(pointA * Mathf.Deg2Rad);

        if (Vector2.Dot((fixedPoint - point).normalized, hitInfo.leftHitNormal) < 0)
            return fixedPoint - hitInfo.toPointDirection * point1c;
        else
            return fixedPoint + hitInfo.toPointDirection * point1c;
    }

    private List<Vector2> GetVisiblePointsOfColliders()
    {
        Collider2D[] colliders = Physics2D.OverlapCircleAll((Vector2)transform.position, radius, blockLayerMask, detectionDepthSection.x, detectionDepthSection.y);
        List<Vector2> points = new List<Vector2>();
        foreach (Collider2D collider in colliders)
        {
            List<Vector2> pointsOfCollider = new List<Vector2>();
            if (collider is BoxCollider2D)
            {
                BoxCollider2D boxCollider = (BoxCollider2D)collider;
                pointsOfCollider.Add(new Vector2(-boxCollider.size.x, -boxCollider.size.y) * 0.5f);
                pointsOfCollider.Add(new Vector2(boxCollider.size.x, boxCollider.size.y) * 0.5f);
                pointsOfCollider.Add(new Vector2(-boxCollider.size.x, boxCollider.size.y) * 0.5f);
                pointsOfCollider.Add(new Vector2(boxCollider.size.x, -boxCollider.size.y) * 0.5f);
            }
            else if (collider is EdgeCollider2D)
            {
                EdgeCollider2D edgeCollider = (EdgeCollider2D)collider;
                edgeCollider.GetPoints(pointsOfCollider);
            }
            else if (collider is PolygonCollider2D)
            {
                PolygonCollider2D polygonCollider = (PolygonCollider2D)collider;
                pointsOfCollider.AddRange(polygonCollider.points);
            }
            Vector2 center = transform.position;
            foreach (Vector2 point in pointsOfCollider)
            {
                Vector2 worldPoint = collider.gameObject.transform.TransformPoint(point);
                if (!InFanAngleAndDistance(worldPoint))
                    continue;
                if (IsVisiblePoint(worldPoint, true))
                {
                    points.Add(worldPoint);
                    Debug.DrawLine(transform.position, worldPoint, Color.green);
                }
                else
                    Debug.DrawLine(transform.position, worldPoint, Color.red);
            }
        }
        return points;
    }

    private bool IsVisiblePoint(Vector2 point, bool offset)
    {
        Vector2 closePoint;
        if (offset)
        {
            Vector2 toCenterDirection = ((Vector2)transform.position - point).normalized;
            closePoint = point + toCenterDirection * detectionOffset;
        }
        else
        {
            closePoint = point;
        }
        RaycastHit2D raycastHit = Physics2D.Linecast(closePoint, transform.position, blockLayerMask | (1 << gameObject.layer), detectionDepthSection.x, detectionDepthSection.y);
        return raycastHit && raycastHit.collider == selfCollider;
    }

    private bool InFanAngleAndDistance(Vector2 point)
    {
        if (fanShape)
            if (Vector2.Angle(transform.up, (point - (Vector2)transform.position).normalized) > fanAngle / 2)
                return false;
        if (Vector3.Distance(transform.position, point) > radius)
            return false;
        return true;
    }

    private float GetAngle360(Vector2 dir1, Vector2 dir2)
    {
        float angle = Vector2.Angle(dir1, dir2);
        dir1 = Quaternion.AngleAxis(90, Vector3.forward) * dir1;
        if (Vector2.Dot(dir1, dir2) < 0)
        {
            angle = 360 - angle;
        }
        return angle;
    }

    private void CompleteHitInfo(HitInfo hitInfo)
    {
        hitInfo.toPointDirection = (hitInfo.basicPoint - (Vector2)transform.position).normalized;
        if (hitInfo.isAssistPoint)
            return;
        Vector2 perpendicular = Vector2.Perpendicular(hitInfo.toPointDirection);

        hitInfo.leftPoint = hitInfo.basicPoint + perpendicular * detectionOffset;
        hitInfo.rightPoint = hitInfo.basicPoint - perpendicular * detectionOffset;

        hitInfo.toLeftPointDirection = (hitInfo.leftPoint - (Vector2)transform.position).normalized;
        hitInfo.toRightPointDirection = (hitInfo.rightPoint - (Vector2)transform.position).normalized;

        RaycastHit2D leftPointRaycastHit = Physics2D.Raycast(transform.position, hitInfo.toLeftPointDirection, radius, blockLayerMask, detectionDepthSection.x, detectionDepthSection.y);
        RaycastHit2D rightPointRaycastHit = Physics2D.Raycast(transform.position, hitInfo.toRightPointDirection, radius, blockLayerMask, detectionDepthSection.x, detectionDepthSection.y);

        if (leftPointRaycastHit)
        {
            hitInfo.leftHitNormal = leftPointRaycastHit.normal;
            hitInfo.leftHit = true;
            hitInfo.leftHitPoint = leftPointRaycastHit.point;
        }
        if (rightPointRaycastHit)
        {
            hitInfo.rightHitNormal = rightPointRaycastHit.normal;
            hitInfo.rightHit = true;
            hitInfo.rightHitPoint = rightPointRaycastHit.point;
        }

        if (debug)
        {
            Debug.DrawLine(transform.position, hitInfo.leftPoint, Color.yellow);
            Debug.DrawLine(transform.position, hitInfo.rightPoint, Color.red);
        }
    }

    private float PointToRayDistance(Vector3 start, Vector2 direction, Vector3 point)
    {
        Vector3 p1_target = point - start;
        return Mathf.Sin(Vector3.Angle(direction, p1_target) * Mathf.Deg2Rad) * p1_target.magnitude;
    }
}